// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CollapsiblePanel.cs" company="">
//   
// </copyright>
// <summary>
//   The collapsible panel.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace OVT.CustomControls
{
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Windows.Forms;

    using OVT.CustomControls.Properties;

    /// <summary>
    /// The collapsible panel.
    /// </summary>
    [Designer(typeof(CollapsiblePanelDesigner))]
    [ToolboxBitmap(typeof(CollapsiblePanel), "OVT.CustomControls.CollapsiblePanel.bmp")]
    [DefaultProperty("HeaderText")]
    public partial class CollapsiblePanel : Panel
    {
        #region "Private members"

        /// <summary>
        /// The collapse.
        /// </summary>
        private bool collapse;

        /// <summary>
        /// The header corners radius.
        /// </summary>
        private int headerCornersRadius = 10;

        /// <summary>
        /// The header font.
        /// </summary>
        private Font headerFont;

        /// <summary>
        /// The header image.
        /// </summary>
        private Image headerImage;

        /// <summary>
        /// The header text.
        /// </summary>
        private string headerText;

        /// <summary>
        /// The header text auto ellipsis.
        /// </summary>
        private bool headerTextAutoEllipsis;

        /// <summary>
        /// The header text color.
        /// </summary>
        private Color headerTextColor;

        /// <summary>
        /// The original hight.
        /// </summary>
        private int originalHight;

        /// <summary>
        /// The rounded corners.
        /// </summary>
        private bool roundedCorners;

        /// <summary>
        /// The show header separator.
        /// </summary>
        private bool showHeaderSeparator = true;

        /// <summary>
        /// The tool tip rectangle.
        /// </summary>
        private RectangleF toolTipRectangle;

        /// <summary>
        /// The use animation.
        /// </summary>
        private bool useAnimation;

        /// <summary>
        /// The use tool tip.
        /// </summary>
        private bool useToolTip;

        #endregion

        #region "Public Properties"

        /// <summary>
        /// Gets or sets BackColor.
        /// </summary>
        [Browsable(false)]
        public new Color BackColor
        {
            get
            {
                return Color.Transparent;
            }

            set
            {
                base.BackColor = Color.Transparent;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether Collapse.
        /// </summary>
        [DefaultValue(false)]
        [Description("Collapses the control when set to true")]
        [Category("CollapsiblePanel")]
        public bool Collapse
        {
            get
            {
                return this.collapse;
            }

            set
            {
                // If using animation make sure to ignore requests for collapse or expand while a previous
                // operation is in progress.
                if (this.useAnimation)
                {
                    // An operation is already in progress.
                    if (this.timerAnimation.Enabled)
                    {
                        return;
                    }
                }

                this.collapse = value;
                this.CollapseOrExpand();
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets AnimationInterval.
        /// </summary>
        [DefaultValue(50)]
        [Category("CollapsiblePanel")]
        [Description(
            "Specifies the speed (in ms) of Expand/Collapse operation when using animation. UseAnimation property must be set to true."
            )]
        public int AnimationInterval
        {
            get
            {
                return this.timerAnimation.Interval;
            }

            set
            {
                // Update animation interval only during idle times.
                if (!this.timerAnimation.Enabled)
                {
                    this.timerAnimation.Interval = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether UseAnimation.
        /// </summary>
        [DefaultValue(false)]
        [Category("CollapsiblePanel")]
        [Description("Indicate if the panel uses amination during Expand/Collapse operation")]
        public bool UseAnimation
        {
            get
            {
                return this.useAnimation;
            }

            set
            {
                this.useAnimation = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether ShowHeaderSeparator.
        /// </summary>
        [DefaultValue(true)]
        [Category("CollapsiblePanel")]
        [Description(
            "When set to true draws panel borders, and shows a line separating the panel's header from the rest of the control"
            )]
        public bool ShowHeaderSeparator
        {
            get
            {
                return this.showHeaderSeparator;
            }

            set
            {
                this.showHeaderSeparator = value;
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether RoundedCorners.
        /// </summary>
        [DefaultValue(false)]
        [Category("CollapsiblePanel")]
        [Description(
            "When set to true, draws a panel with rounded top corners, the radius can bet set through HeaderCornersRadius property"
            )]
        public bool RoundedCorners
        {
            get
            {
                return this.roundedCorners;
            }

            set
            {
                this.roundedCorners = value;
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets HeaderCornersRadius.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// </exception>
        [DefaultValue(10)]
        [Category("CollapsiblePanel")]
        [Description("Top corners radius, it should be in [1, 15] range")]
        public int HeaderCornersRadius
        {
            get
            {
                return this.headerCornersRadius;
            }

            set
            {
                if (value < 1 || value > 15)
                {
                    throw new ArgumentOutOfRangeException(
                        "HeaderCornersRadius", value, "Value should be in range [1, 90]");
                }
                else
                {
                    this.headerCornersRadius = value;
                    this.Refresh();
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether HeaderTextAutoEllipsis.
        /// </summary>
        [DefaultValue(false)]
        [Category("CollapsiblePanel")]
        [Description("Enables the automatic handling of text that extends beyond the width of the label control.")]
        public bool HeaderTextAutoEllipsis
        {
            get
            {
                return this.headerTextAutoEllipsis;
            }

            set
            {
                this.headerTextAutoEllipsis = value;
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets HeaderText.
        /// </summary>
        [Category("CollapsiblePanel")]
        [Description("Text to show in panel's header")]
        public string HeaderText
        {
            get
            {
                return this.headerText;
            }

            set
            {
                this.headerText = value;
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets HeaderTextColor.
        /// </summary>
        [Category("CollapsiblePanel")]
        [Description("Color of text header, and panel's borders when ShowHeaderSeparator is set to true")]
        public Color HeaderTextColor
        {
            get
            {
                return this.headerTextColor;
            }

            set
            {
                this.headerTextColor = value;
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets HeaderImage.
        /// </summary>
        [Category("CollapsiblePanel")]
        [Description("Image that will be displayed in the top left corner of the panel")]
        public Image HeaderImage
        {
            get
            {
                return this.headerImage;
            }

            set
            {
                this.headerImage = value;
                this.Refresh();
            }
        }

        /// <summary>
        /// Gets or sets HeaderFont.
        /// </summary>
        [Category("CollapsiblePanel")]
        [Description("The font used to display text in the panel's header.")]
        public Font HeaderFont
        {
            get
            {
                return this.headerFont;
            }

            set
            {
                this.headerFont = value;
                this.Refresh();
            }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="CollapsiblePanel"/> class.
        /// </summary>
        public CollapsiblePanel()
        {
            this.InitializeComponent();
            this.pnlHeader.Width = this.Width - 1;

            this.headerFont = new Font(this.Font, FontStyle.Bold);
            this.headerTextColor = Color.Black;
        }

        /// <summary>
        /// The on paint.
        /// </summary>
        /// <param name="e">
        /// The e.
        /// </param>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            this.DrawHeaderPanel(e);
        }

        /// <summary>
        /// The draw header corners.
        /// </summary>
        /// <param name="g">
        /// The g.
        /// </param>
        /// <param name="brush">
        /// The brush.
        /// </param>
        /// <param name="x">
        /// The x.
        /// </param>
        /// <param name="y">
        /// The y.
        /// </param>
        /// <param name="width">
        /// The width.
        /// </param>
        /// <param name="height">
        /// The height.
        /// </param>
        /// <param name="radius">
        /// The radius.
        /// </param>
        public void DrawHeaderCorners(
            Graphics g, Brush brush, float x, float y, float width, float height, float radius)
        {
            var gp = new GraphicsPath();

            gp.AddLine(x + radius, y, x + width - (radius * 2), y); // Line
            gp.AddArc(x + width - (radius * 2), y, radius * 2, radius * 2, 270, 90); // Corner
            gp.AddLine(x + width, y + radius, x + width, y + height); // Line
            gp.AddLine(x + width, y + height, x, y + height); // Line
            gp.AddLine(x, y + height, x, y + radius); // Line
            gp.AddArc(x, y, radius * 2, radius * 2, 180, 90); // Corner
            gp.CloseFigure();
            g.FillPath(brush, gp);
            if (this.showHeaderSeparator)
            {
                g.DrawPath(new Pen(this.headerTextColor), gp);
            }

            gp.Dispose();
        }

        /// <summary>
        /// The draw header panel.
        /// </summary>
        /// <param name="e">
        /// The e.
        /// </param>
        private void DrawHeaderPanel(PaintEventArgs e)
        {
            Rectangle headerRect = this.pnlHeader.ClientRectangle;
            var headerBrush = new LinearGradientBrush(
                headerRect, Color.Snow, Color.LightBlue, LinearGradientMode.Horizontal);

            if (!this.roundedCorners)
            {
                e.Graphics.FillRectangle(headerBrush, headerRect);
                if (this.showHeaderSeparator)
                {
                    e.Graphics.DrawRectangle(new Pen(this.headerTextColor), headerRect);
                }
            }
            else
            {
                this.DrawHeaderCorners(
                    e.Graphics, 
                    headerBrush, 
                    headerRect.X, 
                    headerRect.Y, 
                    headerRect.Width, 
                    headerRect.Height, 
                    this.headerCornersRadius);
            }

            // Draw header separator
            if (this.showHeaderSeparator)
            {
                var start = new Point(this.pnlHeader.Location.X, this.pnlHeader.Location.Y + this.pnlHeader.Height);
                var end = new Point(
                    this.pnlHeader.Location.X + this.pnlHeader.Width, this.pnlHeader.Location.Y + this.pnlHeader.Height);
                e.Graphics.DrawLine(new Pen(this.headerTextColor, 2), start, end);

                // Draw rectangle lines for the rest of the control.
                Rectangle bodyRect = this.ClientRectangle;
                bodyRect.Y += this.pnlHeader.Height;
                bodyRect.Height -= this.pnlHeader.Height + 1;
                bodyRect.Width -= 1;
                e.Graphics.DrawRectangle(new Pen(this.headerTextColor), bodyRect);
            }

            int headerRectHeight = this.pnlHeader.Height;

            // Draw header image.
            if (this.headerImage != null)
            {
                this.pictureBoxImage.Image = this.headerImage;
                this.pictureBoxImage.Visible = true;
            }
            else
            {
                this.pictureBoxImage.Image = null;
                this.pictureBoxImage.Visible = false;
            }

            // Calculate header string position.
            if (!String.IsNullOrEmpty(this.headerText))
            {
                this.useToolTip = false;
                int delta = this.pictureBoxExpandCollapse.Width + 5;
                int offset = 0;
                if (this.headerImage != null)
                {
                    offset = headerRectHeight;
                }

                var headerTextPosition = new PointF();
                Size headerTextSize = TextRenderer.MeasureText(this.headerText, this.headerFont);
                if (this.headerTextAutoEllipsis)
                {
                    if (headerTextSize.Width >= headerRect.Width - (delta + offset))
                    {
                        var rectLayout = new RectangleF(
                            (float)headerRect.X + offset, 
                            (float)(headerRect.Height - headerTextSize.Height) / 2, 
                            (float)headerRect.Width - delta, 
                            headerTextSize.Height);
                        var format = new StringFormat();
                        format.Trimming = StringTrimming.EllipsisWord;
                        e.Graphics.DrawString(
                            this.headerText, this.headerFont, new SolidBrush(this.headerTextColor), rectLayout, format);

                        this.toolTipRectangle = rectLayout;
                        this.useToolTip = true;
                    }
                    else
                    {
                        headerTextPosition.X = (offset + headerRect.Width - headerTextSize.Width) / 2;
                        headerTextPosition.Y = (headerRect.Height - headerTextSize.Height) / 2;
                        e.Graphics.DrawString(
                            this.headerText, this.headerFont, new SolidBrush(this.headerTextColor), headerTextPosition);
                    }
                }
                else
                {
                    headerTextPosition.X = (offset + headerRect.Width - headerTextSize.Width) / 2;
                    headerTextPosition.Y = (headerRect.Height - headerTextSize.Height) / 2;
                    e.Graphics.DrawString(
                        this.headerText, this.headerFont, new SolidBrush(this.headerTextColor), headerTextPosition);
                }
            }
        }

        /// <summary>
        /// The picture box expand collapse_ click.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void pictureBoxExpandCollapse_Click(object sender, EventArgs e)
        {
            this.Collapse = !this.Collapse;
        }

        /// <summary>
        /// The collapse or expand.
        /// </summary>
        private void CollapseOrExpand()
        {
            if (!this.useAnimation)
            {
                if (this.collapse)
                {
                    this.originalHight = this.Height;
                    this.Height = this.pnlHeader.Height + 3;
                    this.pictureBoxExpandCollapse.Image = Resources.expand;
                }
                else
                {
                    this.Height = this.originalHight;
                    this.pictureBoxExpandCollapse.Image = Resources.collapse;
                }
            }
            else
            {
                // Keep original height only in case of a collapse operation.
                if (this.collapse)
                {
                    this.originalHight = this.Height;
                }

                this.timerAnimation.Enabled = true;
                this.timerAnimation.Start();
            }
        }

        /// <summary>
        /// The picture box expand collapse_ mouse move.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void pictureBoxExpandCollapse_MouseMove(object sender, MouseEventArgs e)
        {
            if (!this.timerAnimation.Enabled)
            {
                if (!this.collapse)
                {
                    this.pictureBoxExpandCollapse.Image = Resources.collapse_hightlight;
                }
                else
                {
                    this.pictureBoxExpandCollapse.Image = Resources.expand_highlight;
                }
            }
        }

        /// <summary>
        /// The picture box expand collapse_ mouse leave.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void pictureBoxExpandCollapse_MouseLeave(object sender, EventArgs e)
        {
            if (!this.timerAnimation.Enabled)
            {
                if (!this.collapse)
                {
                    this.pictureBoxExpandCollapse.Image = Resources.collapse;
                }
                else
                {
                    this.pictureBoxExpandCollapse.Image = Resources.expand;
                }
            }
        }

        /// <summary>
        /// The on resize.
        /// </summary>
        /// <param name="e">
        /// The e.
        /// </param>
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            this.pnlHeader.Width = this.Width - 1;
            this.Refresh();
        }

        /// <summary>
        /// The on size changed.
        /// </summary>
        /// <param name="e">
        /// The e.
        /// </param>
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            this.pnlHeader.Width = this.Width - 1;
            this.Refresh();
        }

        /// <summary>
        /// The timer animation_ tick.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void timerAnimation_Tick(object sender, EventArgs e)
        {
            if (this.collapse)
            {
                if (this.Height <= this.pnlHeader.Height + 3)
                {
                    this.timerAnimation.Stop();
                    this.timerAnimation.Enabled = false;
                    this.pictureBoxExpandCollapse.Image = Resources.expand;
                }
                else
                {
                    int newHight = this.Height - 20;
                    if (newHight <= this.pnlHeader.Height + 3)
                    {
                        newHight = this.pnlHeader.Height + 3;
                    }

                    this.Height = newHight;
                }
            }
            else
            {
                if (this.Height >= this.originalHight)
                {
                    this.timerAnimation.Stop();
                    this.timerAnimation.Enabled = false;
                    this.pictureBoxExpandCollapse.Image = Resources.collapse;
                }
                else
                {
                    int newHeight = this.Height + 20;
                    if (newHeight >= this.originalHight)
                    {
                        newHeight = this.originalHight;
                    }

                    this.Height = newHeight;
                }
            }
        }

        /// <summary>
        /// The pnl header_ mouse hover.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void pnlHeader_MouseHover(object sender, EventArgs e)
        {
            if (this.useToolTip)
            {
                Point p = this.PointToClient(MousePosition);
                if (this.toolTipRectangle.Contains(p))
                {
                    this.toolTip.Show(this.headerText, this.pnlHeader, p);
                }
            }
        }

        /// <summary>
        /// The pnl header_ mouse leave.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void pnlHeader_MouseLeave(object sender, EventArgs e)
        {
            if (this.useToolTip)
            {
                Point p = this.PointToClient(MousePosition);
                if (!this.toolTipRectangle.Contains(p))
                {
                    this.toolTip.Hide(this.pnlHeader);
                }
            }
        }
    }
}